package jmine.tec.services.validation;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.lang.reflect.TypeVariable;
import java.util.HashSet;
import java.util.Set;

import jmine.tec.datadigester.positional.PositionalFileFiller;
import jmine.tec.datadigester.positional.meta.PositionalFieldMetadata;
import jmine.tec.datadigester.positional.meta.PositionalLayoutMetadataFactory;
import jmine.tec.datadigester.positional.meta.PositionalLineMetadata;
import jmine.tec.di.annotation.Injected;
import jmine.tec.di.scope.TypedScope;
import jmine.tec.persist.PersistMessages;
import jmine.tec.services.annotations.Execution;
import jmine.tec.services.annotations.Output;
import jmine.tec.services.annotations.ServiceImplementor;
import jmine.tec.services.api.ActionsEnum;
import jmine.tec.services.api.ServiceExecutionException;
import jmine.tec.services.impl.ServicesImplMessages;
import jmine.tec.services.test.services.validar.PositionalTestService;
import jmine.tec.utils.reflection.GenericTypeResolverImpl;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;

import bancosys.tec.exception.CompoundMessageHolder;
import bancosys.tec.persist.exception.PersistenceException;

/**
 * Base positional line validation service class. The subclass must supply a annotated bean as his type parameter T 
 * and a string representing a file positional line.
 * The used annotations must be supported by {@link PositionalFileFiller}. The annotations description, start and size
 * will be used to validate the given positional file. Example using the {@link PositionalTestService}:
 *
 *                          Param NOME    Param INTEIRO Param LONG 
 *   VALIDAR PositionalTest VWXYZ         123           9876
 *
 * The 'Param' prefix is necessary to verify wich service parameters must be validated, matching the generated bean annotated fields.
 * 
 * @author seiti
 * Jun 29, 2012
 *
 * @param <T> the annotated bean. The annotations must be supported by {@link PositionalFileFiller}.
 */
@ServiceImplementor(action = ActionsEnum.VALIDAR)
public abstract class AbstractPositionalValidationService<T> {

    private Logger log = LogManager.getLogger(this.getClass());

    public static final String IDENTIFIER = "IDENTIFICADOR";
    
    public static final String DEFAULT_PARAM_PREFIX = "Param";
    
    // Fields
    private TypedScope typedProperties;

    private CompoundMessageHolder validationErrorMessages = new CompoundMessageHolder();
    
    /**
     * Validates
     * 
     * @return the bean representing the parsed line
     */
    @Output(propertyName=IDENTIFIER)
    @Execution   
    public T validate() throws ServiceExecutionException {
        T bean = this.createBean();
        new PositionalFileFiller().parse(bean, this.getLineToBeValidated());
        validate(bean, this.typedProperties);
        return bean;
    }
    
    /**
     * The service parameter to be matched with the positional value is supposed to have a prefix,
     * used to distinguish them from the other service parameters. 
     * 
     * @param prefix prefix
     * @param rawField rawField
     * @return the adjusted service field name
     */
    public static String adjustToServiceField(String prefix, String rawField) {
        if (StringUtils.isEmpty(prefix)) {
            return rawField;
        }
        return prefix + " " + rawField;
    }
    
    @Injected
    public void setTypedScope(TypedScope typedProperties) {
        this.typedProperties = typedProperties;
    }

    /**
     * Must return the line to be validated. It's contents will set the T created bean fields. 
     *
     * @return a string representing the positional line to be validated.
     */
    protected abstract String getLineToBeValidated();

    /**
     * Overrides this if the parameters prefix has to be changed. An empty string can be supplied, but
     * all the service parameters will be used to validate the positional line.
     *
     * @return the service parameters prefix
     */
    protected String getParamPrefix() {
        return DEFAULT_PARAM_PREFIX;
    }
    
    /**
     * Validates the beans properties using the service parameters values.
     * 
     * @param bean to be validated
     * @param typedProperties represents the service fields 
     * @throws ServiceExecutionException if coulf not validate
     */
    private void validate(T bean, TypedScope typedProperties) throws ServiceExecutionException {
        PositionalLayoutMetadataFactory metadataFactory = new PositionalLayoutMetadataFactory();
        PositionalLineMetadata metadata = metadataFactory.forBean(bean.getClass());
        Set<String> matchedFields = new HashSet<String>();
        for (PositionalFieldMetadata field : metadata.getFields()) {
            String serviceField = this.adjustToServiceField(field.description());
            if (typedProperties.containsKey(serviceField)) {
                Object actual = field.get(bean);
                Object expected = typedProperties.get(serviceField, actual.getClass());
                this.validateSingleField(expected, actual);
                matchedFields.add(serviceField);
            }
        }
        
        Set<String> paramFields = new HashSet<String>(); 
        for (String k : typedProperties.getProperties().keySet()) {
            if (!StringUtils.isEmpty(k) && k.startsWith(this.getParamPrefix() + " ")) {
                paramFields.add(k);
            }
        }
        
        paramFields.removeAll(matchedFields);
        if (!paramFields.isEmpty()) {
            final String unmatchedFields = StringUtils.join(paramFields.toArray(new String[0]), ", ");
            throw new ServiceExecutionException(ServicesImplMessages.VALIDATION_NOT_ALL_FIELDS_WERE_VALIDATED.create(unmatchedFields));
        }
    }
    
    private void validateSingleField(final Object expected, final Object actual) {
        if (expected == null || actual == null) {
            this.validationErrorMessages.addMessage(ServicesImplMessages.ASSERT_SAME_FAILED.create(expected, actual));
        }
        if (expected.equals(actual)) {
            this.log.debug("Campo " + expected + " validado.");
        } else {
            this.validationErrorMessages.addMessage(ServicesImplMessages.ASSERT_SAME_FAILED.create(expected, actual));
        }
    }
    
    private String adjustToServiceField(String rawField) {
        return adjustToServiceField(this.getParamPrefix(), rawField);
    }
    
    @SuppressWarnings("unchecked")
    private T createBean() {
        T bean;
        try {
            Class clazz = this.getTargetBeanClass();
            Constructor constructor = clazz.getDeclaredConstructor((Class[]) null);
            if (!Modifier.isPublic(constructor.getModifiers()) || !Modifier.isPublic(constructor.getDeclaringClass().getModifiers())) {
                constructor.setAccessible(true);
            }
            bean = (T) constructor.newInstance(new Object[0]);
        } catch (InstantiationException e) {
            throw new PersistenceException(PersistMessages.UNABLE_TO_INSTANTIATE_BEAN.create(this.getTargetBeanClass().getName()), e);
        } catch (IllegalAccessException e) {
            throw new PersistenceException(PersistMessages.UNABLE_TO_INSTANTIATE_BEAN.create(this.getTargetBeanClass().getName()), e);
        } catch (SecurityException e) {
            throw new PersistenceException(PersistMessages.UNABLE_TO_INSTANTIATE_BEAN.create(this.getTargetBeanClass().getName()), e);
        } catch (NoSuchMethodException e) {
            throw new PersistenceException(PersistMessages.UNABLE_TO_INSTANTIATE_BEAN.create(this.getTargetBeanClass().getName()), e);
        } catch (IllegalArgumentException e) {
            throw new PersistenceException(PersistMessages.UNABLE_TO_INSTANTIATE_BEAN.create(this.getTargetBeanClass().getName()), e);
        } catch (InvocationTargetException e) {
            throw new PersistenceException(PersistMessages.UNABLE_TO_INSTANTIATE_BEAN.create(this.getTargetBeanClass().getName()), e);
        }
        return bean;
    }

    @SuppressWarnings("unchecked")
    private Class<? extends T> getTargetBeanClass() {
        try {
            TypeVariable typeVariable = AbstractPositionalValidationService.class.getTypeParameters()[0];
            return (Class<? extends T>) GenericTypeResolverImpl.getInstance().resolveTypeForFixedGenericType(typeVariable, this.getClass());
        } catch (IllegalArgumentException e) {
            throw new PersistenceException(PersistMessages.INVALID_BEAN_IMPLEMENTATION.create(this.getClass()));
        }
    }
}
